package de.uni_stuttgart.ipvs.ids.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;

/**
 * Class for creating a socket connection to a server for computing sums.
 * @author Team 3/4 - Elkhan Dadashov, Dhaval A Shah, Patrick Stortz
 */
public class SocketClient {

	/** The name to be used to identify this instance */
	private static final String mName = "Socket Client";
	/** The timeout in milliseconds to be used for the socket connect */
	private static final int TIMEOUT_TCP_CONNECT = 5000;

	/** The Socket to use to connect to the server */
	private Socket mClientSocket = null;
	/** The PrintWriter to be used to write to the socket output stream */
	private PrintWriter mOutputWriter = null;
	/** The BufferedReader to be use to read from the socket input stream */
	private BufferedReader mInputReader = null;
	/** Indicates whether we are connected to the server socket or not */
	private boolean mConnected = false;

	/** @return the mName */
	public String getName() {
		return mName;
	}

	/**
	 * Creates a client socket and establishes a connection to the server socket.
	 * @param server the IP address of the server socket
	 * @param port the port of the server socket
	 */
	public SocketClient(String server, short port) {

		// create an unconnected socket
		mClientSocket = new Socket();
		
		// try to connect to the server socket (blocking for CommunicationManager.TIMEOUT_TCP_CONNECT milliseconds)
		try {
			System.out.println(getName() + " | Connecting to the server...");// DEBUG LOG OUTPUT
			mClientSocket.connect(new InetSocketAddress(server, port), TIMEOUT_TCP_CONNECT);
		} catch (Exception e) {
			System.err.println(getName() + " | Could not connect to the server " + server + ":" + port + "! Exception: " + e.getMessage());// DEBUG LOG OUTPUT
			mClientSocket=null;
		}

		if(mClientSocket!=null && mClientSocket.isConnected()){
			System.out.println(getName() + " | Connected to the server " + mClientSocket.getRemoteSocketAddress());// DEBUG LOG OUTPUT
			
			// get the output writer
			try {
				System.out.println(this.getName()+" | Getting the output writer...");// DEBUG LOG OUTPUT
				mOutputWriter = new PrintWriter(mClientSocket.getOutputStream(), true);
			} catch (IOException e) {
				System.err.println(this.getName()+" | Could not get the output writer! Exception: " + e.getMessage());// DEBUG LOG OUTPUT
				mOutputWriter=null;
			}
			
			// get the input reader
			try {
				System.out.println(this.getName()+" | Getting the input reader...");// DEBUG LOG OUTPUT
				mInputReader = new BufferedReader(new InputStreamReader(mClientSocket.getInputStream()));
			} catch (IOException e) {
				System.err.println(this.getName()+" | Could not get the input reader! Exception: " + e.getMessage());// DEBUG LOG OUTPUT
				mInputReader=null;
			}
			
			// check if both streams are connected
			if(mOutputWriter!=null&&mInputReader!=null){
				System.out.println(this.getName()+" | Socket connection successfully established.");// DEBUG LOG OUTPUT
			}

		}
	}

	/**
	 * This method talks to the remote summation service to compute the sum.
	 * @param numbers The numbers to sum up.
	 * @return The computed sum.
	 * @throws IOException
	 * @throws SummationErrorException
	 */
	public int computeSum(Collection<Integer> numbers) throws IOException, SummationErrorException {
		int result = -1;
		String request = null;
		String reply = null;
		
		// check if we didn't get a ready signal yet
		if(mConnected==false){
			// wait for the ready
			reply = this.receiveString();
			// check if the message is a ready signal
			if(reply.equals("READY")){
				mConnected = true;
			}else{
				throw new SummationErrorException("The received message is not a 'READY' ("+reply+")!");
			}
		}

		// get the current sum
		int currentSum = 0;
		request = "SUM";
		this.sendString(request + " ");
		// wait for the reply and check it
		reply = this.receiveString();
		if(reply.startsWith("OK" + " " + request)){
			try{	
				// decode the sum within the message
				currentSum = Integer.decode(reply.substring(7));
			}catch(NumberFormatException ex){
				throw new SummationErrorException("The received message is not a valid integer ("+reply+")!");
			}
		}else{
			throw new SummationErrorException("The request '"+request+"' was not successfull ("+reply+")!");
		}
		
		// add the numbers
		for(Integer number : numbers){
			// send the integer request
			request = number.toString();
			sendString(request + " ");
			// wait for the reply and check it
			reply = this.receiveString();
			if(reply.startsWith("OK" + " " + request) == false){
				throw new SummationErrorException("The request '"+request+"' was not successfull ("+reply+")!");
			}
		}
		
		// get the sum again
		int newSum = 0;
		request = "SUM";
		this.sendString(request + " ");
		// wait for the reply and check it
		reply = this.receiveString();
		if(reply.startsWith("OK" + " " + request)){
			try{	
				// decode the sum within the message
				newSum = Integer.decode(reply.substring(7));
			}catch(NumberFormatException ex){
				throw new SummationErrorException("The received message is not a valid integer ("+reply+")!");
			}
		}else{
			throw new SummationErrorException("The request '"+request+"' was not successfull ("+reply+")!");
		}
		
		// calculate the difference of the last sum and the new one
		result = newSum - currentSum;	
		
		return result;
	}
	
	/**
	 * Receives a string from the server socket.
	 * @return the string received from the server socket.
	 */
	private String receiveString(){
		String received = null;
		if(mInputReader!=null){
			try {
				//System.out.println(this.getName()+" | Reading from the input stream...");// DEBUG LOG OUTPUT
				received = mInputReader.readLine();
				///System.out.println(this.getName()+" | Read following message from input stream: '"+received+"'.");// DEBUG LOG OUTPUT
			} catch (IOException e) {
				// we cannot read from the stream anymore --> connection closed!
				System.err.println(this.getName()+" | Could not read from the input stream! Exception: " + e.getMessage());// DEBUG LOG OUTPUT
				mConnected=false;
				received=null;
			}
		}
		return received;
	}

	/**
	 * Sends a string to the server socket.
	 * @param string the string to send to the server socket.
	 */
	private void sendString(String string){
		if(mOutputWriter!=null){
			//System.out.println(this.getName()+" | Writing to the output stream...");// DEBUG LOG OUTPUT
			mOutputWriter.println(string);
			//System.out.println(this.getName()+" | Wrote following message to the output stream: '"+string+"'.");// DEBUG LOG OUTPUT
		}
	}

	/**
	 * Closes the connection to the server.
	 * Since it is not allowed to change the main method, it is not used.
	 */
	public void closeConnection(){
		// close the output writer
		if(mOutputWriter!=null) {
			System.out.println(this.getName()+" | Closing the output writer...");// DEBUG LOG OUTPUT
			mOutputWriter.close();
			mOutputWriter=null;
		}

		// close the input reader
		if(mInputReader!=null){
			try {
				System.out.println(this.getName()+" | Closing the input reader...");// DEBUG LOG OUTPUT
				mInputReader.close();
				mInputReader=null;
			} catch (IOException e) {
				System.err.println(this.getName()+" | Could not close the input reader! Exception: " + e.getMessage());// DEBUG LOG OUTPUT
			}
		}

		// close the socket
		if(mClientSocket!=null) {
			try {
				System.out.println(this.getName()+" | Closing the socket...");// DEBUG LOG OUTPUT
				mClientSocket.close();
				mClientSocket=null;		
			} catch (IOException e) {
				System.err.println(this.getName()+" | Could not close its socket to a client! Exception: " + e.getMessage());// DEBUG LOG OUTPUT
			}
		}
	}

	/*
	 * Main program.
	 * DO NOT CHANGE THIS METHOD!
	 */
	public static void main(String[] argv) throws Exception {
		if (argv.length != 2) {
			System.err.println("Wrong number of arguments. Usage: ClientMain <serverIP> <serverPort>");
			System.exit(-1);
		}
		short port = Short.parseShort(argv[1]);
		System.out.println("Starting SocketClient on " + argv[0] + ":" + port);
		SocketClient client = new SocketClient(argv[0], port);

		runTest(client);
	}

	/*
	 * Test method.
	 * DO NOT CHANGE THIS METHOD!
	 */
	public static void runTest(SocketClient client) throws IOException {
		try {
			System.out.println("Computing...");
			Collection<Integer> numbers1 = new ArrayList<Integer>(6);
			for (int i = 1; i < 6; ++i) {
				numbers1.add(i);
			}
			int result1 = client.computeSum(numbers1);
			System.out.println("Result 1 is " + result1);
		} catch (SummationErrorException e) {
			System.err.println("Error during summation 1.");
			e.printStackTrace();
		}

		try {
			System.out.println("Computing...");
			Collection<Integer> numbers2 = new ArrayList<Integer>(3);
			numbers2.add(5);
			numbers2.add(-3);
			numbers2.add(4);
			int result2 = client.computeSum(numbers2);
			System.out.println("Result 2 is " + result2);
		} catch (SummationErrorException e) {
			System.err.println("Error during summation 2.");
			e.printStackTrace();
		}

		try {
			System.out.println("Computing...");
			Collection<Integer> numbers3 = new ArrayList<Integer>(50);
			for (int i = 0; i < 50; ++i) {
				numbers3.add((int) ((Math.random() * 200) - 100));
			}
			int result3 = client.computeSum(numbers3);
			System.out.println("Result 3 is " + result3);
		} catch (SummationErrorException e) {
			System.err.println("Error during summation 3.");
			e.printStackTrace();
		}
	}

}
